/*
 * Flashlib - QB9 common Flash library
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.qb9.flashlib.input.mouse
{
	import com.qb9.flashlib.interfaces.IDisposable;
	import com.qb9.flashlib.interfaces.IMoveable;
	
	import flash.display.DisplayObject;
	import flash.display.DisplayObjectContainer;
	import flash.display.InteractiveObject;
	import flash.display.Stage;
	import flash.events.MouseEvent;
	import flash.geom.Point;
	import flash.utils.getTimer;
	
	/**
	 * Provides a programmatic mouse that can be moved
	 * and its left button can be pressed and released.
	 * 
	 * When moved and/or actioned, it will dispatch all the 
	 * standard events, a normal mouse would dispatch.
	 */
	
	public class VirtualMouse implements IMoveable, IDisposable
	{
		// Windows' default
		private static const MAX_DBLCLICK_DELAY:uint = 500;

		private var position:Point = new Point();
		private var stage:Stage;
		
		private var lastPressed:DisplayObject;
		private var lastClicked:DisplayObject;
		private var lastHovered:DisplayObject;
		
		private var lastClick:int;
		
		/** The mouse requires either a cursor object or the stage */
		public function VirtualMouse(stage:Stage)
		{
			this.stage = stage;
		}
		
		/** The mouse X coordinate */
		public function get x():Number
		{ 
			return position.x; 
		}
		
		/** @private */
		public function set x(v:Number):void
		{ 
			moveTo(v, y);
		}
		
		/** The mouse Y coordinate */
		public function get y():Number
		{ 
			return position.y; 
		}
		
		/** @private */
		public function set y(v:Number):void
		{ 
			moveTo(x, v);
		}
		
		/** 
		 * Moves the mouse to a new coordinate.
		 * Setting the x and y attributes instead will trigger the events twice.
		 * This function is mainly provided to improve perfomance.
		 */
		public function moveTo(x:Number, y:Number):void
		{
			position.x = x;
			position.y = y;
			update();
		}
		
		/** Whether the mouse is pressed */
		public function get isDown():Boolean
		{
			return lastPressed !== null;
		}
		
		/** Returns the current element below the mouse cursor */
		public function get target():InteractiveObject
		{	
			var objects:Array = stage.getObjectsUnderPoint(position).reverse();
			for each (var elem:DisplayObject in objects)
			{
				var object:InteractiveObject = elem as InteractiveObject;
				if (!object || !object.mouseEnabled)
					continue;
					
				var ancestor:DisplayObjectContainer = object.parent;
				var ok:Boolean = true;
				while (ok && ancestor)
				{
					ok &&= ancestor.mouseChildren;
					ancestor = ancestor.parent;
				}
				
				if (ok)
					return object;
			}
			
			return stage;
		}
		
		private function dispatchMouse(object:DisplayObject, event:String, bubbles:Boolean=true, related:DisplayObject=null):void
		{
			var needed:Boolean = bubbles ? object.willTrigger(event) : object.hasEventListener(event);
			if (!needed)
				return;
			
			var local:Point = object.globalToLocal(position);

			// Removed support for ctrlKey, altKey, shiftKey and mouseDown, seemed pointless
			object.dispatchEvent(new VirtualMouseEvent(
				event, bubbles, true, local.x, local.y, related as InteractiveObject
			));
		}
		
		/** Presses the mouse button, triggers mouseDown */
		public function press():void
		{
			if (isDown) return;

			lastPressed = target;
			dispatchMouse(lastPressed, MouseEvent.MOUSE_DOWN);
		}
		
		/** Releases the mouse button, triggers mouseUp and sometimes click and doubleClick */
		public function release():void
		{
			if (!isDown) return;
			
			var object:InteractiveObject = target;
			dispatchMouse(object, MouseEvent.MOUSE_UP);
			
			var changedTarget:Boolean = object !== lastPressed;
			lastPressed = null;
			
			if (changedTarget)
				return;
				
			dispatchMouse(object, MouseEvent.CLICK);
			
			if (object === lastClicked && getTimer() - lastClick <= MAX_DBLCLICK_DELAY)
			{
				if (object.doubleClickEnabled)
					dispatchMouse(object, MouseEvent.DOUBLE_CLICK);
				lastClicked = null;
			}
			else
			{
				lastClicked = object;
				lastClick = getTimer();
			}
		}
		
		/** Generates a click */
		public function click():void
		{
			press();
			release();
		}
		
		private function update():void
		{
			var prev:DisplayObject = lastHovered;
			var current:DisplayObject = lastHovered = target;

			dispatchMouse(current, MouseEvent.MOUSE_MOVE);
			
			if (prev)
			{
				if (prev === current) return;
			
				dispatchMouse(prev, MouseEvent.MOUSE_OUT, true, current);
				
				checkRolling(prev, current, MouseEvent.ROLL_OUT);
				checkRolling(current, prev, MouseEvent.ROLL_OVER);
			} 
			
			dispatchMouse(current, MouseEvent.MOUSE_OVER, true, prev);
		}
		
		private function checkRolling(elem:DisplayObject, related:DisplayObject, event:String):void
		{
			while (elem.parent && !elem.parent.contains(related))
			{
				dispatchMouse(elem, event, false, related);
				elem = elem.parent;
			}
		}
		
		public function dispose():void
		{
			lastClicked = lastHovered = lastPressed = stage = null;
		}
	}
}
